/*
**************************************************************************************
*Myriad Engine                                                                       *
*Copyright (C) 2006-2007, 5d Studios (www.5d-Studios.com)                            *
*                                                                                    *
*This library is free software; you can redistribute it and/or                       *
*modify it under the terms of the GNU Lesser General Public                          *
*License as published by the Free Software Foundation; either                        *
*version 2.1 of the License, or (at your option) any later version.                  *
*                                                                                    *
*This library is distributed in the hope that it will be useful,                     *
*but WITHOUT ANY WARRANTY; without even the implied warranty of                      *
*MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU                   *
*Lesser General Public License for more details.                                     *
*                                                                                    *
*You should have received a copy of the GNU Lesser General Public                    *
*License along with this library; if not, write to the Free Software                 *
*Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA      *
**************************************************************************************
*/
package leola.backend.interpreter;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;

import leola.ast.ASTAttributes;
import leola.ast.ASTNode;
import leola.ast.ASTNodeVisitor;
import leola.ast.ArrayAccessExpr;
import leola.ast.ArrayAccessSetExpr;
import leola.ast.ArrayDeclExpr;
import leola.ast.AssignmentExpr;
import leola.ast.BinaryExpr;
import leola.ast.BooleanExpr;
import leola.ast.BreakStmt;
import leola.ast.ClassDeclStmt;
import leola.ast.CompoundStmt;
import leola.ast.EmptyStmt;
import leola.ast.Expr;
import leola.ast.FuncDefExpr;
import leola.ast.FuncInvocationExpr;
import leola.ast.IfStmt;
import leola.ast.MemberAccessExpr;
import leola.ast.NewExpr;
import leola.ast.NullExpr;
import leola.ast.NumberExpr;
import leola.ast.ReturnStmt;
import leola.ast.StringExpr;
import leola.ast.UnaryExpr;
import leola.ast.ValDeclStmt;
import leola.ast.VarDeclStmt;
import leola.ast.VarExpr;
import leola.ast.WhileStmt;
import leola.backend.EvalException;
import leola.types.LeoArray;
import leola.types.LeoBoolean;
import leola.types.LeoClass;
import leola.types.LeoFunction;
import leola.types.LeoNativeClass;
import leola.types.LeoNativeFunction;
import leola.types.LeoNumber;
import leola.types.LeoObject;
import leola.types.LeoObject.LeoType;
import leola.utils.ClassUtil;



/**
 * @author Tony
 *
 */
public class Interpreter implements ASTNodeVisitor {

	private VM vm;
	private Callstack callstack;
	private SymbolStack symbolStack;
	
	/**
	 * Executes functions
	 */
	private FunctionExecutor functionExecutor;
	
	/**
	 * Class Defs
	 */
	private ClassDefinitions classDefinitions;
	
	/**
	 */
	public Interpreter() {
		this(new VM());
	}
	
	/**
	 * @param vm
	 */
	public Interpreter(VM vm) {
		this.vm = vm;
		this.callstack = vm.getCallstack();
		this.symbolStack = vm.getSymstack();
		
		this.functionExecutor = new FunctionExecutor(this);
		this.classDefinitions = new ClassDefinitions(this);		
	}

	/**
	 * @return the callstack
	 */
	public Callstack getCallstack() {
		return callstack;
	}
	
	/**
	 * @return the symbolStack
	 */
	public SymbolStack getSymbolStack() {
		return symbolStack;
	}
	
	/**
	 * @return the classDefinitions
	 */
	public ClassDefinitions getClassDefinitions() {
		return classDefinitions;
	}
	
	/**
	 * Does a shallow copy
	 * @return a shallow copy
	 */
	public Interpreter shallowClone() {
		synchronized (this) {
			return new Interpreter(this.vm.shallowClone());
		}
	}
	
	/**
	 * Adds a {@link LeoNativeFunction} to the global scope.
	 * 
	 * @param functionName
	 * @param func
	 */
	public void addLeoNativeFunction(String functionName, LeoNativeFunction func) {
		this.symbolStack.getGlobal().getSymbolTable().put(functionName, func);
	}
	
	/**
	 * Loads the class
	 * @param className
	 */
	public void loadClass(Class<?> aClass) throws Exception {		
		List<Method> methods = ClassUtil.getAllDeclaredMethods(aClass);
		for(Method m: methods) {
			LeoNativeFunction func = new LeoNativeFunction(aClass, m.getParameterTypes(), m.getName());
			boolean isStatic = (m.getModifiers() & Modifier.STATIC) != 0;
			if ( isStatic ) {
				addLeoNativeFunction(m.getName(), func);
			}
			else {
				addLeoNativeFunction(aClass.getName() + "$" + m.getName(), func);
			}
		}
	}
	
	/**
	 * Loads the class
	 * 
	 * @param className
	 */
	public void loadClass(String className) throws Exception {
		Class<?> aClass = Class.forName(className);
		loadClass(aClass);
	}
	
	/**
	 * Visits with a new scope.
	 * 
	 * @param node
	 * @param embed
	 */
	private void visitWithLocalScope(ASTNode node, boolean embed) throws Exception {
		if ( node != null ) {
			this.symbolStack.push(new Scope(embed));
			node.visit(this);
			this.symbolStack.pop();
		}
	}
	
	
	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.AssignmentStmt)
	 */
	@Override
	public void visit(AssignmentExpr s) throws Exception {		
		Expr expr = s.getExpr();
		
		/* If this is a member access, we can pop off the 
		 * parent symbols and push them back
		 * after we are done.
		 */
		Object attr = s.getAttribute(ASTAttributes.MEMBER_ACCESS);
		if ( attr != null  ) {
			Scope leftHandSideScope = this.symbolStack.pop();
			expr.visit(this);	
			this.symbolStack.push(leftHandSideScope);
		}				
		else {
			expr.visit(this);
		}
		
		String variableName = s.getVarName();
		
		/* if this is an array/map index we must pop two
		 * values from the stack
		 */
		Expr lhs = s.getLhsExpr();
		if ( lhs != null  ) {
			LeoObject newValue = this.callstack.pop();	// the new value
			
			lhs.visit(this);
			LeoObject index = this.callstack.pop();		// the index into the map/array
			LeoObject obj = this.callstack.pop();		// the complex object (map or array)
									
			if ( obj.isOfType(LeoType.ARRAY) ) {
				LeoArray array = obj.as();
				LeoNumber i = index.as();
				array.set(i, newValue);
			}
			else if ( obj.isOfType(LeoType.MAP) ) {
				/* TODO */
			}
			else {
				throw new EvalException(s, "Invalid complex object access for: " + obj);
			}
			
			this.callstack.push(obj);
		}
		else {
			/* This is a reference assignment on a variable */			
			LeoObject newValue = this.callstack.peek();
			if ( ! this.symbolStack.set(variableName, newValue) ) {
				throw new EvalException(s, "No variable defined for: '" + s.getVarName() +"'");
			}
		}
		
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.CompoundStmt)
	 */
	@Override
	public void visit(CompoundStmt s) throws Exception {
		for(ASTNode child : s.getChildren()) {			
			
			child.visit(this);
			
			/* if this is an expression not attached to 
			 * another statement we need to pop off the stack
			 * 
			 * Ex.
			 * myClass.invoke(); // without popping here this corrupts the stack
			 */
			if ( child instanceof Expr && !this.callstack.empty()) {
				this.callstack.pop();
			}
		}
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.ConstExpr)
	 */
	@Override
	public void visit(NumberExpr s) throws Exception {
		this.callstack.push(new LeoNumber(s.getValue()));
	}
 
	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.EmptyStmt)
	 */
	@Override
	public void visit(EmptyStmt s) throws Exception {	
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.FuncDefExpr)
	 */
	@Override
	public void visit(FuncDefExpr s) throws Exception {
		LeoFunction functor = new LeoFunction(s.getParameters(), s.getBody(), this.symbolStack.peek());
		this.callstack.push(functor);
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.FuncInvocationExpr)
	 */
	@Override
	public void visit(FuncInvocationExpr s) throws Exception {
		String functionName = s.getFunctionName();
		String ownerName = s.getOwner();
		
		LeoObject value = this.symbolStack.lookup(functionName);
		
		/* Free function invokation is not defined */
		if ( value == null && ownerName == null ) {		
			throw new EvalException
				("The function: " + functionName + " does not exist in this scope!");
		}
		else if ( ownerName != null ) {
			String className = "";
			
			LeoObject ownerObj = this.symbolStack.lookup(ownerName);
			if ( ownerObj == null ) {
				throw new EvalException("Parent not defined: " + ownerObj);
			}
			
			boolean isNative = ownerObj.isOfType(LeoType.NATIVE_CLASS); 
			if ( isNative ) {
				LeoNativeClass nativeClass = ownerObj.as();
				className = nativeClass.getNativeClass().getName();
				
				String memberFunctionName = className + "$" + functionName;
				
				value = this.symbolStack.lookup(memberFunctionName);
				if ( value == null ) {
//					if ( isNative ) {
						LeoObject params[] = parseParameters(s.getParameters());
//						LeoNativeClass nativeClass = ownerObj.as();
						this.functionExecutor.executeUnknownNativeFunction(nativeClass, functionName, params);
						return;
//					}
//					else {
//						throw new EvalException
//							("The member function: " + memberFunctionName + " does not exist in this scope!");
//					}
				}
				
			}
			else if ( ownerObj.isOfType(LeoType.CLASS)) {
				LeoClass aClass = ownerObj.as();
				value = aClass.getScope().getSymbolTable().get(functionName);
				if ( value == null ) {
					throw new EvalException("The member function: " + functionName + " does not exist in this scope!");
				}
			}
			else {
				throw new EvalException("Invalid Parent type: " + ownerObj.getType() + " - " + ownerObj);
			}
			

		}
		
		if ( value.isOfType(LeoType.FUNCTION)) {
			LeoFunction func = value.as();
			LeoObject params[] = parseParameters(s.getParameters());
			
			executeFunction(func, params);	
		}
		else if ( value.isOfType(LeoType.NATIVE_FUNCTION)) {
			LeoNativeFunction func = value.as();
			LeoObject params[] = parseParameters(s.getParameters());						
			LeoNativeClass owner = null;
			
			
			if ( ownerName != null ) {
				LeoObject obj = this.symbolStack.lookup(ownerName);
				if ( obj != null ) {
					if ( obj.isOfType(LeoType.NATIVE_CLASS) ) {
						owner = obj.as();
					}
					else {
						throw new EvalException
							("Invalid function: " + functionName + ". The supplied instance if not a NATIVE_CLASS: " + obj);			
					}
				}
			}
			
			executeNativeFunction(owner, functionName, func, params);
		}
		else {
			throw new EvalException("Invalid function: " + functionName + ". Not of FUNCTION or NATIVE_FUNCTION type!");
		}	
	
		
	}
	
	/**
	 * Executes the native function.
	 * 
	 * @param function
	 * @param params
	 * @throws Exception
	 */
	private void executeNativeFunction(LeoNativeClass owner, String functionName, LeoNativeFunction function, LeoObject ... params) throws Exception {
		this.functionExecutor.executeNativeFunction(owner, functionName, function, params);		
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.IfStmt)
	 */
	@Override
	public void visit(IfStmt s) throws Exception {
		s.getCondition().visit(this);
		
		LeoObject retval = this.callstack.pop();
				
		/* If the return value is true */
		if ( retval.isTrue() ) {			
			visitWithLocalScope(s.getStmt(), true);
		}
		else {
			visitWithLocalScope(s.getElseStmt(), true);
		}
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.MemberAccessExpr)
	 */
	@Override
	public void visit(MemberAccessExpr s) throws Exception {
		
		String parentVarName = s.getParent();
		LeoObject obj = this.symbolStack.lookup(parentVarName);
		if ( obj == null ) {
			throw new EvalException(s, 
					"The value '" + s.getParent() + "' is not in scope or is not defined.");
		}
		
		/* The parent pushes its local symbol table
		 * so functions can have access to the instance members 
		 * of the class
		 */
		if ( obj.isOfType(LeoType.CLASS)) {
			LeoClass parentClass = obj.as();
			Scope scope = parentClass.getScope();
			boolean previous = scope.setEmbedded(true);
			this.symbolStack.push(scope);
			{
				/* This is a class access, so attach the class name for
				 * symbol recognition
				 */
				Expr expr = s.getAccess();
				if ( expr instanceof AssignmentExpr ) {
					expr.setAttribute(ASTAttributes.MEMBER_ACCESS, true);				
				}
				
				expr.visit(this);												
			}
			this.symbolStack.pop();
			scope.setEmbedded(previous);			
		}
		else if ( obj.isOfType(LeoType.NATIVE_CLASS) ) {
//			LeoNativeClass parentClass = obj.as();			
			
			{
				/* This is a class access, so attach the class name for
				 * symbol recognition
				 */
				Expr expr = s.getAccess();
				if ( expr instanceof AssignmentExpr ) {
					expr.setAttribute(ASTAttributes.MEMBER_ACCESS, true);				
				}
				
				expr.visit(this);												
			}			
		}
		else {
			s.getAccess().visit(this);
		}
		
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.ReturnStmt)
	 */
	@Override
	public void visit(ReturnStmt s) throws Exception {
		s.getExpr().visit(this);
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.VarDeclStmt)
	 */
	@Override
	public void visit(VarDeclStmt s) throws Exception {
		s.getValue().visit(this);
		
		String variableName = s.getVarName();

		LeoObject obj = this.callstack.pop();
		this.symbolStack.peek().getSymbolTable().put(variableName, obj);				
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.WhileStmt)
	 */
	@Override
	public void visit(WhileStmt s) throws Exception {
		
		boolean cont = false;
		do {
			s.getCondition().visit(this);			
			LeoObject condition = this.callstack.pop();	
			if ( condition.isTrue() ) {
				cont = true;				
				visitWithLocalScope(s.getStmt(), true);
			}
			else {
				cont = false;
			}
		}
		while( cont );
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.BinaryExpr)
	 */
	@Override
	public void visit(BinaryExpr s) throws Exception {
		s.getLeft().visit(this);
		LeoObject leftObj = this.callstack.pop();
		
		s.getRight().visit(this);
		LeoObject rightObj = this.callstack.pop();
		
		switch(s.getOp()) {
		case ADD:
			if ( leftObj.isOfType(LeoType.STRING) || rightObj.isOfType(LeoType.STRING)) {							
				this.callstack.push("" + leftObj.getValue() + rightObj.getValue());				
			}
			else if ( leftObj.isOfType(LeoType.NUMBER) && 
					  rightObj.isOfType(LeoType.NUMBER) ) {
				LeoNumber ln = leftObj.as();
				LeoNumber rn = rightObj.as();
				this.callstack.push( ln.getNumber() + rn.getNumber() ); 
			}		
			else if ( leftObj.isOfType(LeoType.ARRAY) ) {
				LeoArray array = leftObj.as();
				array.add(rightObj);
				this.callstack.push(array);
			}
			else {
				throw new EvalException("Unable to ADD: " + leftObj + " , " + rightObj);
			}
			break;
		case AND:
			this.callstack.push( leftObj.isTrue() && rightObj.isTrue() ); 						
			break;
		case BIT_AND:
			if ( leftObj.isOfType(LeoType.NUMBER) && 
				 rightObj.isOfType(LeoType.NUMBER) ) {
					
				LeoNumber ln = leftObj.as();
				LeoNumber rn = rightObj.as();

				int l = (int)ln.getNumber();
				int r = (int)rn.getNumber();
				
				this.callstack.push( l & r ); 			
			}			
			else {
				throw new EvalException("Unable to BITWISE AND: " + leftObj + " , " + rightObj);
			}				
			break;
		case BIT_OR:
			if ( leftObj.isOfType(LeoType.NUMBER) && 
				 rightObj.isOfType(LeoType.NUMBER) ) {
					
				LeoNumber ln = leftObj.as();
				LeoNumber rn = rightObj.as();

				int l = (int)ln.getNumber();
				int r = (int)rn.getNumber();
				
				this.callstack.push( l | r ); 			
			}			
			else {
				throw new EvalException("Unable to BITWISE OR: " + leftObj + " , " + rightObj);
			}				
			break;
		case BIT_XOR:
			if ( leftObj.isOfType(LeoType.NUMBER) && 
				 rightObj.isOfType(LeoType.NUMBER) ) {
					
				LeoNumber ln = leftObj.as();
				LeoNumber rn = rightObj.as();

				int l = (int)ln.getNumber();
				int r = (int)rn.getNumber();
				
				this.callstack.push( l ^ r ); 			
			}			
			else {
				throw new EvalException("Unable to BITWISE XOR: " + leftObj + " , " + rightObj);
			}				
			break;			
		case DIV:
			if ( leftObj.isOfType(LeoType.NUMBER) && 
				 rightObj.isOfType(LeoType.NUMBER) ) {
					
				LeoNumber ln = leftObj.as();
				LeoNumber rn = rightObj.as();
				
				if ( rn.getNumber() == 0 ) {
					throw new EvalException("Divide by zero error");
				}
				
				this.callstack.push( ln.getNumber() / rn.getNumber() ); 			
			}			
			else {
				throw new EvalException("Unable to DIVIDE: " + leftObj + " , " + rightObj);
			}
			break;
		case MOD:
			if ( leftObj.isOfType(LeoType.NUMBER) && 
				 rightObj.isOfType(LeoType.NUMBER) ) {
					
				LeoNumber ln = leftObj.as();
				LeoNumber rn = rightObj.as();
				
				if ( rn.getNumber() == 0 ) {
					throw new EvalException("Divide by zero error");
				}
				
				this.callstack.push( ln.getNumber() % rn.getNumber() ); 			
			}			
			else {
				throw new EvalException("Unable to MODULO: " + leftObj + " , " + rightObj);
			}							
			break;
		case MUL:
			if ( leftObj.isOfType(LeoType.NUMBER) && 
				 rightObj.isOfType(LeoType.NUMBER) ) {
					
				LeoNumber ln = leftObj.as();
				LeoNumber rn = rightObj.as();

				this.callstack.push( ln.getNumber() * rn.getNumber() ); 			
			}			
			else {
				throw new EvalException("Unable to MULTIPLY: " + leftObj + " , " + rightObj);
			}				
			break;
		case OR:
			this.callstack.push( leftObj.isTrue() || rightObj.isTrue() );
			break;
		case SUB:
			if ( leftObj.isOfType(LeoType.NUMBER) && 
				 rightObj.isOfType(LeoType.NUMBER) ) {
					
				LeoNumber ln = leftObj.as();
				LeoNumber rn = rightObj.as();

				this.callstack.push( ln.getNumber() - rn.getNumber() ); 			
			}			
			else {
				throw new EvalException("Unable to SUBTRACT: " + leftObj + " , " + rightObj);
			}	
			break;						
		case EQ: {
			this.callstack.push(leftObj.eq(rightObj));
			break;
		}
		case NEQ: {
			this.callstack.push(leftObj.neq(rightObj));
			break;
		}
		case GT: {
			this.callstack.push(leftObj.gt(rightObj));
			break;
		}
		case GTE: {
			this.callstack.push(leftObj.gte(rightObj));
			break;
		}
		case LT: {
			this.callstack.push(leftObj.lt(rightObj));
			break;
		}
		case LTE: {
			this.callstack.push(leftObj.lte(rightObj));
			break;
		}	

		default:
			throw new EvalException("Unknown BinaryOp!");
		}
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.BooleanExpr)
	 */
	@Override
	public void visit(BooleanExpr s) throws Exception {
		this.callstack.push(s.getValue());
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.StringExpr)
	 */
	@Override
	public void visit(StringExpr s) throws Exception {
		this.callstack.push(s.getValue());
	}

	/**
	 * Executes a function
	 * 
	 * @param function
	 * @param params
	 * @throws Exception
	 */
	public void executeFunction(LeoFunction function, LeoObject ... params) throws Exception {
		this.functionExecutor.executeFunction(function, params);
	}

	
	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.VarExpr)
	 */
	@Override
	public void visit(VarExpr s) throws Exception {
		String variableName = s.getVarName();
				
		LeoObject obj = this.symbolStack.lookup(variableName);
		if ( obj == null ) {
			throw new EvalException("No variable defined for: " + s.getVarName());
		}
		this.callstack.push(obj);
	}

	
	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.NewExpr)
	 */
	@Override
	public void visit(NewExpr s) throws Exception {
		String className = s.getClassName();
		if ( this.classDefinitions.isClass(className) ) {
			LeoObject[] params = parseParameters(s.getParameters());
			LeoClass aClass = this.classDefinitions.newInstance(className, params);
			
			this.callstack.push(aClass);
		}
		/* Lets see if this is a native class */
		else if ( this.classDefinitions.isNativeClass(className)) {
			LeoObject[] params = parseParameters(s.getParameters());
			LeoNativeClass aClass = this.classDefinitions.newNativeInstance(className, params);
			this.callstack.push(aClass);
		}
		else {
			throw new EvalException("No class defined for: " + className);
		}
	}

	/**
	 * Parses the parameters
	 * 
	 * @param params
	 * @return
	 * @throws Exception
	 */
	private LeoObject[] parseParameters(Expr[] params) throws Exception {
		LeoObject[] results = new LeoObject[params.length];
		
		/*
		 * Parse each parameter
		 */
		for(int i = 0; i < params.length; i++ ) {
			Expr expr = params[i];
			expr.visit(this);
			
			results[i] = this.callstack.pop();
		}
		
		return (results);
	}
	
	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.ValDeclStmt)
	 */
	@Override
	public void visit(ValDeclStmt s) throws Exception {
		s.getValue().visit(this);
		
		String variableName = s.getVarName();

		LeoObject obj = this.callstack.pop();
		obj.setFinal(true);
		
		this.symbolStack.peek().getSymbolTable().put(variableName, obj);
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.NullExpr)
	 */
	@Override
	public void visit(NullExpr s) throws Exception {
		this.callstack.pushNull();
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.UnaryExpr)
	 */
	@Override
	public void visit(UnaryExpr s) throws Exception {
		s.getExpr().visit(this);
		
		LeoObject obj = this.callstack.pop();
		
		
		switch(s.getOp()) {
			case NEGATE: {
				if ( obj == null ) {
					throw new EvalException("The unary expression is negating null!");
				}
				
				if ( obj.isOfType(LeoType.NUMBER)) {
					LeoNumber n = obj.as();
					this.callstack.push(-n.getNumber());
				}
				else if ( obj.isOfType(LeoType.BOOLEAN)) {
					LeoBoolean b = obj.as();
					this.callstack.push(!b.isTrue());
				}
				else {
					throw new EvalException("Unable to negate a " + obj.getValue());
				}
				
				break;
			}
			case NOT: {
				if ( obj == null ) {
					this.callstack.push(true); /* ! null == true */
				}
				else if ( obj.isOfType(LeoType.NUMBER)) {
					LeoNumber n = obj.as();
					this.callstack.push( !(n.getNumber()>0) );
				}
				else if ( obj.isOfType(LeoType.BOOLEAN)) {
					LeoBoolean b = obj.as();
					this.callstack.push(!b.isTrue());
				}
				else if ( obj.isOfType(LeoType.NULL) ) {
					this.callstack.push(true);
				}
				else {
					this.callstack.push(false); // ! objects == false 
				}
				
				break;
			}
			case BIT_NOT: {
				if ( obj == null ) {
					throw new EvalException("The unary expression is negating null!");
				}
				
				if ( obj.isOfType(LeoType.NUMBER)) {
					LeoNumber n = obj.as();
					int i = (int)n.getNumber();
					this.callstack.push( ~i );
				}
				else if ( obj.isOfType(LeoType.BOOLEAN)) {
					LeoBoolean b = obj.as();
					this.callstack.push(!b.isTrue());
				}
				else {
					throw new EvalException("Unable to negate a " + obj.getValue());
				}
				
				break;
			}			
		};
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.ClassDeclStmt)
	 */
	@Override
	public void visit(ClassDeclStmt s) throws Exception {		
		this.classDefinitions.store(s);
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.BreakStmt)
	 */
	@Override
	public void visit(BreakStmt s) throws Exception {		
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.ArrayAccessExpr)
	 */
	@Override
	public void visit(ArrayAccessExpr s) throws Exception {
		String variableName = s.getVariableName();
		
		LeoObject obj = this.symbolStack.lookup(variableName);
		if ( obj == null ) {
			throw new EvalException(s, "No variable defined for: " + variableName);
		}
		
		if ( ! obj.isOfType(LeoType.ARRAY) ) {
			throw new EvalException(s, "The suupplied variable is not of type array: " + variableName);
		}
		
		s.getElementIndex().visit(this);
		
		LeoObject element = this.callstack.pop();
		if ( ! element.isOfType(LeoType.NUMBER) ) {
			throw new EvalException(s.getElementIndex(), "Only integer access is allowed for indexing into arrays: " + element);
		}

		LeoNumber index = element.as();
		LeoArray array = obj.as();
		
		LeoObject retObj = array.get( (int)index.getNumber());
		this.callstack.push(retObj);
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.ArrayDeclExpr)
	 */
	@Override
	public void visit(ArrayDeclExpr s) throws Exception {
		LeoArray array = new LeoArray();
		for(Expr expr : s.getElements()) {
			expr.visit(this);
			
			LeoObject obj = this.callstack.pop();
			array.add(obj);
		}
		
		this.callstack.push(array);
	}

	/* (non-Javadoc)
	 * @see leola.ast.ASTNodeVisitor#visit(leola.ast.ArrayAccessSetExpr)
	 */
	@Override
	public void visit(ArrayAccessSetExpr s) throws Exception {
		String variableName = s.getVariableName();
		
		LeoObject obj = this.symbolStack.lookup(variableName);
		if ( obj == null ) {
			throw new EvalException(s, "No variable defined for: " + variableName);
		}
		
		if ( ! obj.isOfType(LeoType.ARRAY) ) {
			throw new EvalException(s, "The supplied variable is not of type array: " + variableName);
		}
		
		s.getElementIndex().visit(this);
		
		LeoObject element = this.callstack.pop();
		if ( ! element.isOfType(LeoType.NUMBER) ) {
			throw new EvalException(s.getElementIndex(), "Only integer access is allowed for indexing into arrays: " + element);
		}

		LeoNumber index = element.as();
		LeoArray array = obj.as();
		
		this.callstack.push(array);
		this.callstack.push(index);		
	}
	
}
